#include <iostream>
#include <fstream>
#include <math.h>
#include <vector>
#include <getopt.h>

//#include <ga/GASimpleGA.h>
//#include <ga/GA1DBinStrGenome.h>
#include "galib247/ga/GASimpleGA.h"
#include "galib247/ga/GA1DBinStrGenome.h"

#define PUGIXML_HEADER_ONLY
#include "pugixml-1.2/src/pugixml.cpp"
#include "pugixml-1.2/src/pugixml.hpp"

#include "main.h"
#include "room.h"

using namespace std;
class Room;

// seznam pokoju
std::vector<Room*> rooms;

// pocet bitu pro pokoje
int maxBits = 6;

// pomocna promenna pro vypocet prikonu telesa
unsigned roomInput;

// pocet vygenerovanych genomu
int fitnessCount = 0;

// soubory
string roomFile;
string outputFile;
string settingFile;


// rozsireny model (parametr -c)
bool complexFitness = false;
// simulacni cas
int heatingTime;
// pouzit implicitni cas (prepina parametr -t)
bool defaultTime = true;

// pripojit na konec vystupniho souboru (parametr -a)
bool append = false;
// vypsat pouze fitness (parametr -f)
bool fitnessOnly = false;

// pomocna promenna pro odhad prubehu
int maxRuns = 0;
// ga bezi
bool running = false;

/*
 * EN
 * This function sets the neigbour temperatures of
 * walls whose neighbours are rooms.
 *
 * CZ
 * Tato funkce nacte sousedni teploty zdem sousedicim s pokoji.
 */
void setNeighbourRoomTemperatures() {

    // byl soused nalezen?
    bool found;

    // projit vsechny pokoje
    for (unsigned int i = 0; i < rooms.size(); i++) {
        Room *room = rooms.at(i);
        // go through all walls
        for (unsigned int j = 0; j < room->walls.size(); j++) {
            Wall *wall = room->walls.at(j);
            // pokud neni sousedem pokoj, dal
            if (wall->neighbour == Wall::Room) {
                // soused zatim nebyl nalezen
                found = false;

                // projit vsechny pokoje a najit pozadovany nazev pokoje
                for (unsigned int k = 0; k < rooms.size(); k++) {
                    Room *tmp = rooms.at(k);
                    // if the name is the one we're looking for
                    if (tmp->name.compare(wall->neighbourRoom) == 0) {
                        // byl nalezen
                        found = true;
                        // nastavit zdi jako vnejsi teplotu teplotu pokoje
                        wall->neighbourTemperature = tmp->currentTemperature;
                        // skoncit hledani
                        break;
                    }
                }

                // pokud nebyl sousedni pokoj nalezen, je chyba v XML
                if (!found) {
                    cout << "Nespravna konfigurace XML." << endl;
                    cout << "Zkontrolujte pokoj \'" << room->name << "\'." << endl;
                    exit(EXIT_FAILURE);
                }
            }
        }
    }
}

/*
 * EN
 * This function loads rooms from the XML file and sets their attributes.
 * string source   - input file
 *
 * CZ
 * Tato funkce nacte z XML pokoje a jejich atributy.
 * string source   - vstupni soubor
 */
void addRooms(string source) {

    pugi::xml_document doc;
    if (!doc.load_file(source.data())) {
        cout << "Soubor \'" << source << "\' se nepodarilo nacist." << endl;
        exit(EXIT_FAILURE);
    }
    else
        cout << "Nacitani pokoju..." << endl;

    pugi::xml_node houseNode = doc.child("house");

    for (pugi::xml_node_iterator roomIt = houseNode.begin(); roomIt != houseNode.end(); ++roomIt)
    {

        // vytvorit novy pokoj
        Room *room = new Room(roomIt->attribute("name").as_string());
        // nastavit aktualni a pozadovanou teplotu
        room->setCurrentTemperature(roomIt->attribute("currentTemperature").as_double());
        room->setDesiredTemperature(roomIt->attribute("desiredTemperature").as_double());

        // projit vsechny zdi v pokoji
        for (pugi::xml_node_iterator wallIt = roomIt->begin(); wallIt != roomIt->end(); ++wallIt) {
            // vlastnosti zdi
            int width = wallIt->attribute("width").as_int();
            int height = wallIt->attribute("height").as_int();
            Wall::Type type = Wall::getTypeFromStr(wallIt->attribute("type").as_string());
            Wall::Neighbour neighbour;
            Wall *wall;

            // pokud se jedna o vnitrni stenu, nalezt k ni sousedni pokoj
            // jinak nacist souseda
            if (type == Wall::Inside) {
                neighbour = Wall::Room;
                wall = new Wall(width, height, type, Wall::Room, wallIt->attribute("neighbour").as_string());
            }
            else {
                neighbour = Wall::getNeighbourFromStr(wallIt->attribute("neighbour").as_string());
                wall = new Wall(width, height, type, neighbour);
            }
            // pridarit zed k pokoji
            room->addWall(wall);
        }
        // pridat do seznamu pokoju
        rooms.push_back(room);
    }
    // nastavit vnejsi
    setNeighbourRoomTemperatures();

    cout << "\r" << "Nacteno " << rooms.size() << " pokoju." << endl << endl;
}

/*
 * EN
 * This function scales the input to <0,1>.
 * char input   - input
 *
 * CZ
 * Tato funkce prevede vstup do intervalu <0,1>.
 * char input   - vstup
 */
double scaleInput(char input) {

    // vstup do intervalu <0,1>
    return static_cast<double>(input) / static_cast<double>(pow(2,maxBits)-1);
}

/*
 * EN
 * This function round the number num to x decimals.
 * double num      - number to be rounded
 * double x        - number of decimals
 *
 * CZ
 * Tato funkce zaokrouhli cislo num na x desetinnych mist.
 * double num      - cislo k zaokrouhleni
 * double x        - pocet desetinnych mist
 */
double roundTo(double num, double x) {
    return round(num*pow(10,x))/pow(10,x);
}

/*
 * EN
 * This function extracts the desired bits from a genome.
 * int start                    - starting position
 * int count                    - number of bits to extract
 * GA1DBinaryStringGenome &g    - input genome
 *
 * CZ
 * Tato funkce extrahuje pozadovane bity z genomu.
 * int start                    - posice, z ktere cheme extrahovat
 * int count                    - pocet bitu, ktere budou extrahovany
 * GA1DBinaryStringGenome &g    - vstupni genom
 */
unsigned getGenomeBits(int start, int count, GAGenome &g) {

    GA1DBinaryStringGenome & genome = (GA1DBinaryStringGenome &)g;

    unsigned number = 0;

    for (int i = start; i < (start+count); i++) {
        number += genome.gene(i);
        if (i < (start+count)-1)
            number = number << 1;
    }

    return number;

}

/*
 * EN
 * New temperature evaluation using the complex model
 * GAGenome &g    - input genome
 * double time    - simulation time
 *
 * CZ
 * ...............
 * GAGenome &g    - vstupni genom
 * double time    - cas simulace
 */
void evaluateComplexModel(GAGenome &g, double time) {

    // provadet po cely simulacni cas
    for (unsigned int i = 0; i < time; i++) {

        // vypocitat teplotu pro kazdy pokoj
        for (unsigned int i = 0; i < rooms.size(); i++) {
            // ziskat bity z genomu
            roomInput = getGenomeBits(maxBits*i, maxBits, g);
            Room *room = rooms.at(i);
            // spocitat pomoci rozsireneho modelu
            room->setCurrentPowerComplex(scaleInput(roomInput));
        }
        // nastavit nove vnejsi teploty pro zdi
        setNeighbourRoomTemperatures();

        // nahradit stare teploty za nove
        for (unsigned int i = 0; i < rooms.size(); i++) {
            Room *room = rooms.at(i);
            room->updateTemperatures();
        }
    }
}

/*
 * EN
 * Fitness function which evaluates the genome.
 * GAGenome &g    - input genome
 *
 * CZ
 * Fitness funkce pro vyhodnoceni genomu.
 * GAGenome &g    - vstupni genom
 */
float fitness(GAGenome &g) {

    float fit = 0.0;
    fitnessCount++;

    // pokud je aktivovat rozsireny model, je treba k vypoctu teplot zavolat evaluateComplexModel()
    if (complexFitness) {
        evaluateComplexModel(g, heatingTime);
    }

    // projit vsechny pokoje
    for (unsigned int i = 0; i < rooms.size(); i++) {
        // ziskat nastaveni pro dany pokoj
        roomInput = getGenomeBits(maxBits*i, maxBits, g);
        Room *room = rooms.at(i);

        // pokud je rozsireny model deaktivovan,
        // spocitat teploty podle jednoducheho modelu
        if (!complexFitness) {
            room->setCurrentPower(scaleInput(roomInput));
        }

        // k hodnote fitness pripocist ctverec rozdilu vypoctene a pozadovane teploty
        // male chyby tak budou male, velke budou velke
        fit += pow((room->newTemperature - room->desiredTemperature),2);
    }

    // odmocneni pro redukci velikosti
    fit = sqrt(fit);
    // prevod na maximalizaci, cim mensi chyba, tim vetsi hodnota fitness
    fit = 1 / (1 + fit);

    // vypis stavu v procentech
    if (running) {
        double percentage = roundTo((((double)fitnessCount / (double)maxRuns)*100),2);
        cout << "\r";
        if (percentage <= 100)
            cout << roundTo((((double)fitnessCount / (double)maxRuns)*100),2);
        else
            cout << 100;
        cout << "%      ";
    }

    return fit;
}

/*
 * EN
 * Print results to desired file.
 * GASimpleGA g     - GA
 * ofstream& stream - output file
 *
 * CZ
 * Vypis vysledku do pozadovaneho souboru.
 * GASimpleGA g     - GA
 * ofstream& stream - vystupni soubor
 */
void printResults(GASimpleGA g, ofstream &stream) {

    // vyber nejlepsiho jedince
    const GAGenome& best_genome = (GAGenome&)g.statistics().bestIndividual();

    // tisknout vsechny vysledky (nejsou zapnuty prepinace -f a -n)
    if (!fitnessOnly) {
        stream << "Nastaveni Genetickeho Algoritmu:" << endl
               << "Velikost populace:       " << g.populationSize() << endl
               << "Pocet generaci:          " << g.nGenerations() << endl
               << "Pravdepodobnost krizeni: " << g.pCrossover() << endl
               << "Pravdepodobnost mutace:  " << g.pMutation() << endl << endl;

        // vypocitat pro rozsireny model, pokud je treba
        if (complexFitness) {
            evaluateComplexModel((GAGenome &)best_genome, heatingTime);
        }

        // nejlepsi fitness
        stream << "Nejlepsi fitness: " << endl;
    }

    double fit = fitness((GAGenome &)best_genome);

    // hodnota fitness
    stream << roundTo(fit, 3) << endl;

    // tisknout vsechny vysledky (nejsou zapnuty prepinace -f a -n)
    if (!fitnessOnly) {
        // chyba ve stupnich
        stream << "Celkova chyba: " << endl;
        double err = roundTo((1/fit - 1), 2);
        stream << err << "°C" << endl;
        stream << "Prumerna chyba na pokoj: " << endl;
        stream << roundTo((err/rooms.size()), 2) << "°C" << endl;

        // vypis vysledku
        stream  << endl
                << "Pokoj\tPrikon telesa (poloha)\tPuvodni teplota\tPozadovana teplota\tNova teplota" << endl;

        // projit vsechny pokoje
        for (unsigned int i = 0; i < rooms.size(); i++) {
            // ziskat nastaveni pro nejlepsiho jedince
            roomInput = getGenomeBits(maxBits*i, maxBits, (GAGenome &)best_genome);
            Room *room = rooms.at(i);

            // vypocet pro jednoduchy model
            if (!complexFitness) {
                room->setCurrentPower(scaleInput(roomInput));
            }

            // vypis vysledku
            stream
                    << " " << room->name
                    << "\t " << roundTo(scaleInput(roomInput)*100, 2) << "%"
                    << " (" << roomInput << "/" << (pow(2,maxBits)-1) << ")"
                    << "\t\t " << room->currentTemperature << "°C"
                    << "\t\t " << room->desiredTemperature << "°C"
                    << "\t\t\t " << roundTo(room->newTemperature, 2) << "°C" << endl;
        }
    }

    // tisknout vsechny vysledky (nejsou zapnuty prepinace -f a -n)
    if (!fitnessOnly) {
        stream << endl;

        stream << "Statistiky Genetickeho Algoritmu:" << endl
               << "Pocet vyberu:            " << g.statistics().selections() << endl
               << "Pocet krizeni:           " << g.statistics().crossovers() << endl
               << "Pocet mutaci:            " << g.statistics().mutations() << endl
               << "Pocet nahrazeni:         " << g.statistics().replacements() << endl
               << "Soubor s ohodnocenim:    \'" << g.statistics().scoreFilename() << "\'" << endl << endl;

        stream.close();
    }
    cout << "Vysledky zapsany do \'" << outputFile << "\'." << endl << endl;
}

/*
 * EN
 * Main.
 *
 * CZ
 * Main.
 */
int main(int argc, char* argv[]) {

    int c;
    // zpracovani vstupnich argumentu
    while ((c = getopt(argc, argv, "afncr:o:s:t:")) != -1) {
        switch (c)
        {
        case 'r':
            // XML soubor s pokoji
            roomFile = optarg;
            break;
        case 'o':
            // vystupni soubor
            outputFile = optarg;
            break;
        case 's':
            // soubor s nastavenim pro GA
            settingFile = optarg;
            break;
        case 't':
            // cas simulace pro rozsireny model
            defaultTime = false;
            heatingTime = atoi(optarg);
            break;
        case 'a':
            // pripojovat vysledky na konec souboru
            append = true;
            break;
        case 'f':
            // vypsat pouze fitness
            fitnessOnly = true;
            append = true;
            break;
        case 'c':
            // aktivovat rozsireny model
            complexFitness = true;
            break;
        default:
            abort();
        }
    }

    if (roomFile.empty()) {
        cout << "XML soubor s pokoji nebyl nastaven." << endl;
        roomFile = "rooms8.xml";
        cout << "Pouziti implicitniho souboru s pokoji: \'";
    }
    else
        cout << "Pouziti XML souboru: \'";
    roomFile = (string)"files/"+roomFile;
    cout << roomFile << "\'." << endl << endl;

    if (outputFile.empty()) {
        cout << "Vystupni soubor nebyl nastaven." << endl;
        outputFile = "garesult.txt";
        cout << "Pouziti implicitniho vystupniho souboru: \'" << outputFile << "\'." << endl;
    }
    else
        cout << "Pouziti vystupniho souboru: \'";
    outputFile = (string)"results/"+outputFile;
    cout << outputFile << "\'." << endl << endl;

    if (settingFile.empty()) {
        cout << "Soubor s nastavenim GA nebyl nastaven." << endl;
        if (!complexFitness)
            settingFile = "settings.txt";
        else
            settingFile = "settingsComplex.txt";
        cout << "Pouziti implicitniho souboru nastaveni: \'" << settingFile << "\'" << endl;
    }
    else
        cout << "Pouziti souboru s nastavenim GA: \'";
    settingFile = (string)"files/"+settingFile;
    cout << settingFile << "\'." << endl << endl;

    if (complexFitness) {
        cout << "Zvolen rozsireny model vypoctu." << endl << endl;
        if (defaultTime) {
            cout << "Simulacni cas nebyl nastaven." << endl;
            heatingTime = 60;
            cout << "Pouziti implicitniho casu: ";
        }
        else
            cout << "Nastaveny cas simulace: ";
        cout << heatingTime << "min" << endl << endl;
    }

    // nacist pokoje z XML
    addRooms(roomFile);

    // delka genomu je maxBits * pocet pokoju
    const int glength = maxBits*rooms.size();

    // prvni genom
    const GA1DBinaryStringGenome first_genome(glength, fitness);
    
    // vygenerovat nahodny prvni genom
    GARandomSeed();
    
    // GA s prvnim genomem
    GASimpleGA g(first_genome);
    // nacist parametry ze souboru
    g.parameters(settingFile.data());
    // nacist parametry ze vstupu
    g.parameters(argc, argv);

    cout << "Nacitani nastaveni..." << endl
         << "Velikost populace:       " << g.populationSize() << endl
         << "Pocet generaci:          " << g.nGenerations() << endl
         << "Pravdepodobnost krizeni: " << g.pCrossover() << endl
         << "Pravdepodobnost mutace:  " << g.pMutation() << endl << endl;

    cout << "Spoustim Geneticky Algoritmus..." << endl;

    // get max runs
    maxRuns = g.populationSize() * g.nGenerations();

    running = true;

    g.evolve();
    if (roundTo((((double)fitnessCount / (double)maxRuns)*100),2) < 100)
        cout << "\r100%    ";
    cout << endl << "Beh Genetickeho Algoritmu dokoncen." << endl << endl;

    // write results to file
    ofstream results;

    // byl zapnut prepinac -a?
    // pokud ano, nebude vystupni soubor prepsan, bude se pokracovat v zapisu
    // na konec souboru
    if (append)
        results.open(outputFile.data(), ios::out | ios::app);
    else
        results.open(outputFile.data());

    running = false;
    // vypsat vysledky
    printResults(g, results);

}
